Skip to content

fix: improve UI responsiveness with large remote hosts files#242

Merged
2ndalpha merged 12 commits intomasterfrom
fix/large-remote-file-performance
Mar 1, 2026
Merged

fix: improve UI responsiveness with large remote hosts files#242
2ndalpha merged 12 commits intomasterfrom
fix/large-remote-file-performance

Conversation

@2ndalpha
Copy link
Owner

@2ndalpha 2ndalpha commented Mar 1, 2026

Summary

Large remote hosts files (~1MB, e.g. StevenBlack/hosts) caused two categories of UI issues: freezes during loading/syntax highlighting, and unresponsive sidebar clicks.

Sidebar click responsiveness

  • Root cause: .onDrag on sidebar rows intercepted mouseDown events, preventing SwiftUI's List from registering clicks on the icon and text — only clicks on empty space selected a row
  • Removed .onDrag from sidebar rows, restoring normal click-to-select behavior

Large file rendering

  • Refactored HostsTextView to highlight large documents (>50KB) asynchronously in ~100KB chunks, yielding to the run loop between each chunk
  • Added O(1) pointer identity check in HostsTextViewRepresentable.updateNSView to skip redundant content replacement
  • Added O(1) NSString.length comparison before O(n) string equality check

Download lifecycle main thread blocking

  • Coalesced rowRefreshToken updates so 9-12 rapid notifications produce one SwiftUI re-render instead of many
  • Removed duplicate HostsFileSavedNotification post in RemoteHostsManager.hostsDownloaded: that caused CombinedHostsController to regenerate and save twice per download
  • Made dscacheutil -flushcache non-blocking using terminationHandler instead of waitUntilExit

Other fixes

  • Dispatched all HostsDownloader delegate callbacks to the main thread to fix data races from NSURLSession background queue
  • Added 24 new tests covering performance, notification coalescing, and file switching behavior

Test plan

  • Add https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts as a remote hosts file
  • Click between local files in the sidebar — clicking on icon/text should select immediately
  • Verify syntax highlighting appears progressively on the large file (colors fill in over ~1-2s)
  • Switch files during async highlighting — no crash
  • Toggle syntax highlighting off/on for the large file — works correctly
  • Edit a small local file — incremental highlighting still works normally
  • Close and reopen the app — clicking between files should be responsive while remote file downloads

2ndalpha added 8 commits March 1, 2026 13:19
Large remote hosts files (e.g. StevenBlack/hosts ~1MB, ~77K domains)
caused the app to become unresponsive due to synchronous syntax
highlighting processing the entire document on the main thread.

- Refactor HostsTextView to highlight large documents (>50KB) in async
  chunks of ~100KB, yielding to the run loop between chunks. A
  generation counter cancels stale passes on file switch or user edit.
- Replace eager .draggable(hosts.contents()) with lazy .onDrag using
  NSItemProvider so sidebar rendering no longer reads file contents.
- Dispatch all HostsDownloader delegate callbacks to the main thread
  to fix data races from NSURLSession background queue callbacks.
- Add O(1) NSString.length check before O(n) string comparison in
  HostsTextViewRepresentable.updateNSView.
Add replaceContentWith: method that bypasses the expensive synchronous
textStorageDidProcessEditing: callback during bulk text replacement.
This avoids the O(n) lineRangeForRange: computation that blocked the
main thread on every file switch. Highlighting is instead triggered
manually after replacement — async for large files, batched sync for
small files.
Add tests proving text view layer is fast:
- Small file switching: ~5ms per switch
- replaceContentWith: no regression vs direct assignment
- No notification cascade during selection changes
- No HostsNodeNeedsUpdate posted during selection
The updateNSView guard was comparing the full text content (O(n)) on
every @published property change, not just selection changes. For a
16K-line file, each comparison took ~22ms, causing visible lag when
multiple re-renders occurred per click.

Fix: decouple HostsTextViewRepresentable from the monolithic store
and use a two-tier guard:
- O(1) pointer check for selection changes (always replace)
- Token-based check for external content updates (compare only when
  rowRefreshToken changes)
- Skip entirely when neither selection nor token changed

Also adds integration tests for the full HostsDataStore → updateNSView
pipeline, objectWillChange publication counting, and pointer-based
guard verification.
…cking

When switching to a large hosts file (>50K chars), replaceContentWith:
called highlightAsyncFrom:0 synchronously, blocking the main thread
for ~20ms on a 1.38MB file. Dispatch the first chunk via
dispatch_async like subsequent chunks, reducing switch time to ~1.5ms.
Three targeted fixes for UI lockup when switching between local files
while a large remote file (e.g. StevenBlack ~1MB) is configured:

- Coalesce rowRefreshToken updates in HostsDataStore so 9-12 rapid
  notifications from a download lifecycle produce 1 SwiftUI re-render
  instead of 9-12
- Remove duplicate HostsFileSavedNotification in RemoteHostsManager
  (hostsController saveHosts: already posts it)
- Make dscacheutil -flushcache non-blocking using terminationHandler
  instead of [task waitUntilExit] (~9ms saved per call)
SwiftUI's .onDrag intercepts mouseDown events, which is the same event
List uses for row selection. This caused clicks on the icon and text
area of sidebar rows to not register as selection — only clicks on
empty space worked, making the UI feel unresponsive.
@2ndalpha 2ndalpha changed the title fix: prevent UI freeze when loading large remote hosts files fix: improve UI responsiveness with large remote hosts files Mar 1, 2026
2ndalpha added 4 commits March 1, 2026 18:11
Restore `import UniformTypeIdentifiers` in SidebarView — the drop
delegate still uses UTType.fileURL and UTType.url. Relying on SwiftUI's
transitive re-export is fragile across SDK versions.

Add logDebug for non-zero dscacheutil exit status so failures are
visible in debug logs instead of silently ignored.
The 50ms per-render assertion was too tight for CI Intel x86_64 runners
where SwiftUI layout in a full NavigationSplitView has high variance
under load. Increase to 200ms to match the concurrent download test
threshold while still catching real regressions.
The macOS 26 Intel runner timed out at ~55s because 30K-line content
triggered expensive async highlighting during RunLoop drains. This test
measures re-render cost per notification, not large file highlighting
(other tests cover that). Reduce to 5K lines and shorten the initial
render wait.
The macOS 26 Intel CI runner consistently times out (~54s) with the full
test suite generating multiple 30K-line strings. Reduce all tests to 5K
lines (175KB), which still exceeds the 50KB async highlight threshold
and exercises the same code paths while being 6x lighter.
@2ndalpha 2ndalpha merged commit 7e0fa13 into master Mar 1, 2026
18 checks passed
@2ndalpha 2ndalpha deleted the fix/large-remote-file-performance branch March 1, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant